A Brief Discussion on .NET Default Logger and Optimization Techniques
Log Levels
The following table defines the various levels and recommended usage scenarios based on Logging in C# and .NET. The None value is the highest, primarily used to completely disable all logs in filter settings.
| Level | Value | Method | Description |
|---|---|---|---|
| Trace | 0 | LogTrace | Contains the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should not be enabled in production environments. |
| Debug | 1 | LogDebug | Used for debugging and development. Due to high volume, use with caution in production environments. |
| Information | 2 | LogInformation | Tracks general application flow. May have long-term value. |
| Warning | 3 | LogWarning | Used for handling abnormal or unexpected events. Usually contains errors or conditions that do not cause the application to fail. |
| Error | 4 | LogError | Unhandled errors and exceptions. These messages indicate a failure in the current operation or request, rather than a failure of the entire application. |
| Critical | 5 | LogCritical | Failures that require immediate attention. Examples: data loss, insufficient disk space. |
| None | 6 | N/A | Specifies that no messages should be written. |
Basic Injection Configuration
WebApplication.CreateBuilder() automatically configures the default Logger upon creation. By examining the .NET source code, specifically the AddDefaultServices() method in HostingHostBuilderExtensions.cs, we can see that the system automatically sets up basic Logger Providers:
// From .NET source code
services.AddLogging(logging => {
bool isWindows =
#if NET
OperatingSystem.IsWindows();
#elif NETFRAMEWORK
Environment.OSVersion.Platform == PlatformID.Win32NT;
#else
RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif
// IMPORTANT: This needs to be added *before* configuration is loaded, this lets
// the defaults be overridden by the configuration.
if (isWindows) {
// Default the EventLogLoggerProvider to warning or above
logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
}
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
#if NET
if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi())
#endif
{
logging.AddConsole();
}
logging.AddDebug();
logging.AddEventSourceLogger();
if (isWindows) {
// Add the EventLogLoggerProvider on windows machines
logging.AddEventLog();
}
logging.Configure(options => {
options.ActivityTrackingOptions =
ActivityTrackingOptions.SpanId |
ActivityTrackingOptions.TraceId |
ActivityTrackingOptions.ParentId;
});
});This means that by default, WebApplication.CreateBuilder() has already configured the following Logger Providers for us:
- Reads settings from the "Logging" section of the Configuration.
- Console Logger (outputs to the console)
- Debug Logger (outputs to the debugger)
- EventSource Logger (used for EventSource tracing)
In practice, we can customize Logger settings as follows:
// CreateBuilder itself has default settings
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// You can adjust added LoggerProviders via DI
builder.Services.AddLogging(logging => {
logging.ClearProviders(); // Clear default providers
logging.AddConsole(); // Add Console Provider
logging.AddDebug(); // Add Debug Provider
});
// Or simplify it as follows
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();How to Inject Logger in Code
In ASP.NET Core, Logger is typically used via dependency injection. Below is a simplified example:
namespace LoggerTest.Controllers {
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase {
private readonly ILogger<UserController> logger;
public UserController(ILogger<UserController> logger) {
this.logger = logger;
this.logger.LogInformation("UserController initialized");
}
}
}Controlling Log Levels Using Appsettings.json
We can configure log levels for different components using the appsettings.json file. Here is an example of a specific scenario using multiple types of Loggers:
namespace LoggerTest.Controllers {
[ApiController]
[Route("api/[controller]")]
public class TestController : Controller {
public TestController(ILogger<TestService1> logger1, ILogger<TestService2> logger2) {
logger1.LogInformation("TestController successfully initialized and injected with Service1's Logger.");
logger2.LogInformation("TestController successfully initialized and injected with Service2's Logger.");
}
}
}
namespace LoggerTest.Services {
public class TestService1 {
public TestService1() {
}
public void TestMethod() {
// Do something
}
}
public class TestService2 {
public TestService2() {
}
public void TestMethod() {
// Do something
}
}
}Combined with the following appsettings.json configuration:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"LoggerTest.Services.TestService1": "Warning"
},
"Debug": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting": "Trace"
}
},
"EventSource": {
"LogLevel": {
"Default": "Warning"
}
}
},
"AllowedHosts": "*"
}In this configuration file:
- The outer "LogLevel" setting applies to all Logger Providers.
- Settings in the "Debug" and "EventSource" sections apply only to the specified Providers.
- "Default" indicates the default minimum log level.
- Specific namespaces like "Microsoft.AspNetCore" can have their own log levels.
After running the program with the above settings, you will find that only Service2's logs are recorded, because Service1's log level is set to Warning.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7109
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5164
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\Programming\Projects\LoggerTest\LoggerTest
info: LoggerTest.Services.TestService2[0]
TestController successfully initialized and injected with Service2's Logger.Using Wildcards for Configuration
If you want to apply the same settings to multiple namespaces, you can use the wildcard *. For example, "*.Services": "Warning" will set the log level to Warning for all namespaces ending in .Services.
Advanced Filtering in Code
If more granular control is needed, you can use Filters in your code:
// Filter by namespace
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
// Filter by provider type
builder.Logging.AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Warning);
builder.Logging.AddFilter<DebugLoggerProvider>("Microsoft.AspNetCore.Mvc", LogLevel.Debug);
// Use wildcards
builder.Logging.AddFilter("Microsoft.*", LogLevel.Warning);
builder.Logging.AddFilter("*.Repository", LogLevel.Debug);
// For a specific class logger
builder.Logging.AddFilter(typeof(Program).FullName, LogLevel.Debug);
// Use more granular control - using functional filters
builder.Logging.AddFilter((provider, category, logLevel) => {
// Show more detailed logs on weekdays
if (DateTime.Now.DayOfWeek != DayOfWeek.Saturday
&& DateTime.Now.DayOfWeek != DayOfWeek.Sunday) {
return logLevel >= LogLevel.Debug;
}
// Only show Warning and above on weekends
return logLevel >= LogLevel.Warning;
});Improving Performance with Logger.IsEnabled
In situations where expensive operations are required to generate log messages, we can check if a specific log level is enabled first to avoid unnecessary computation:
if (logger.IsEnabled(LogLevel.Information)) {
// Potentially resource-intensive operations here, such as database queries or complex calculations
int processedRecords = await database.GetProcessedRecordsCount();
logger.LogInformation("System has finished data update, processed {Count} records in total.", processedRecords);
}The benefit of this is that when the configuration file sets the log level to Warning or higher, we can completely skip these resource-intensive operations.
Advantages and Usage of Structured Logging
Key Benefits of Structured Logging
Performance Advantage: The biggest advantage of structured logging is avoiding unnecessary string concatenation. The Logger only performs string composition when it actually needs to write the log; this is known as "deferred execution." When the log level is set to not record certain levels, the related string processing is never executed, thereby improving application performance.
Data Integration Convenience: Generated logs can be easily parsed by tools like Elasticsearch, Kibana, and Logstash (ELK), facilitating centralized log management.
How to Use Structured Logging
// Visual Studio and .NET analyzers will warn against this via CA2254
logger.LogInformation(
"User " + user.Id + " has logged in, department: " + user.Department
);
// Correct structured logging approach
logger.LogInformation(
"User {UserId} has logged in, department: {Department}",
user.Id,
user.Department
);Special Usage of Curly Braces
If you need to include curly brace characters {} in your log message, you need to use double curly braces to escape them. For example:
// If you want to output text containing curly braces, you need to use double curly braces
logger.LogInformation("This is a JSON example: {{\"name\": \"value\"}}");
// Output result: This is a JSON example: {"name": "value"}
// Note: XXX in {XXX} is used as a variable placeholder, related to the position of the passed parameters
// It is not mapped by parameter name, but by parameter order
logger.LogInformation("Name: {Name}, Age: {Age}", "Xiao Ming", 25);
// Output: Name: Xiao Ming, Age: 25Using JSON Format Output
By replacing AddConsole() with AddJsonConsole(), we can obtain structured JSON output:
builder.Logging.AddConsole();
// Change to
builder.Logging.AddJsonConsole();The generated JSON log format looks like this (formatted for readability):
{
"EventId": 0,
"LogLevel": "Information",
"Category": "LoggerTest.Controllers.TestController",
"Message": "User U12345 has logged in, department: R&D",
"State": {
"UserId": "U12345",
"Department": "R&D",
"{OriginalFormat}": "User {UserId} has logged in, department: {Department}"
},
"Timestamp": "2025-03-23T15:30:00.123Z"
}This structured JSON format is particularly suitable for integration with log analysis systems like ELK, allowing us to search, filter, and analyze logs more effectively. This is also one of the main reasons why Serilog has gradually replaced NLog as the preferred .NET logging framework in recent years.
Change Log
- 2025-03-23 Initial version created.
